﻿//-----------------------------------------------------------------------
// <copyright file="SmartManipulatorTool.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Manipulator.Smart
{
    using TileTool.Action.Message;
    using TileTool.Common.Math;
    using TileTool.Render;
    using TileTool.Scene.Action;
    using TileTool.Scene.Entity;
    using TileTool.Tool;

    /// <summary>
    /// Smart manipulator tool.
    /// </summary>
    public class SmartManipulatorTool : ManipulatorToolBase
    {
        #region Fields - Private

        /// <summary>
        /// Smart manipulator we own.
        /// </summary>
        private SmartManipulator smartManipulator = null;

        #endregion

        #region Constructor

        /// <summary>
        /// Default constructor.
        /// </summary>
        public SmartManipulatorTool() :
            base(new SmartManipulator())
        {
            this.smartManipulator = Manipulator as SmartManipulator;
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Called when a tool is installed into a tool manager. Should be called
        /// by the tool manager after the tool is already in the tool stack.
        /// </summary>
        /// <param name="manager">Tool manager that is hosting the tool. Always non-null.</param>
        public override void Install(IToolManager manager)
        {
            base.Install(manager);
        }

        /// <summary>
        /// Called when a tool is uninstalled from a tool manager. Should be called
        /// by the tool manager after the tool has already been removed form the tool
        /// stack.
        /// </summary>
        public override void Uninstall()
        {
            base.Uninstall();
        }

        /// <summary>
        /// Called to draw the manipulator.
        /// 
        /// The draw call will be made after the manipulator's matrix has been pushed on
        /// to the matrix stack.
        /// </summary>
        /// <param name="renderData">Render data for manipulator drawing.</param>
        public override void Draw(IRenderData renderData)
        {
            base.Draw(renderData);
        }

        #endregion

        #region Methods - Protected - Manipulation

        /// <summary>
        /// Handle a manipulation end.
        /// </summary>
        /// <param name="args">Manipulate event args.</param>
        protected override void ManipulateEnd(ManipulateEventArgs args)
        {
            switch (this.smartManipulator.ManipulateType)
            {
                case SmartManipulateType.Size:
                case SmartManipulateType.Rotate:
                case SmartManipulateType.Translate:
                    break;
                default:
                    base.ManipulateEnd(args);
                    return;
            }

            // If we didn't move at all, don't put a dummy action into the
            // undo redo system.
            Vector2D amount = this.Manipulator.DragAmountWorld;
            if (amount.x == 0 && amount.y == 0)
            {
                return;
            }

            int addedEntities = 0;
            SmartManipulateType type = this.smartManipulator.ManipulateType;
            SetEntityMatrixAndSizeAction action = new SetEntityMatrixAndSizeAction(Scene);

            foreach (ManipulatorEntity manipEnt in this.Entities)
            {
                IEntity entity = manipEnt.Entity;

                if (type == SmartManipulateType.Size)
                {
                    if (!entity.CanSize)
                    {
                        continue;
                    }
                }

                action.AddEntity(entity, manipEnt.StartMatrix, entity.Matrix, manipEnt.StartSize, entity.Size);
                addedEntities++;
            }

            if (addedEntities == 0)
            {
                return;
            }

            action.Do();

            this.SendMessage(new ActionMessage(action));

            // Want this to happen last so that when we end manipulation we have completed all
            // work we need to do here.
            base.ManipulateEnd(args);
        }

        /// <summary>
        /// Handle a manipulation update.
        /// </summary>
        /// <param name="args">Manipulate event args.</param>
        protected override void ManipulateUpdate(ManipulateEventArgs args)
        {
            base.ManipulateUpdate(args);

            SmartManipulateType type = this.smartManipulator.ManipulateType;

            switch (type)
            {
                case SmartManipulateType.Size:
                    this.HandleSizeUpdate();
                    break;
                case SmartManipulateType.Rotate:
                    this.HandleRotateUpdate();
                    break;
                case SmartManipulateType.Translate:
                    this.HandleTranslateUpdate();
                    break;
            }
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Handle size manipulation.
        /// </summary>
        private void HandleSizeUpdate()
        {
            Vector2D translateAmount, sizeAmount;
            bool multiSelect = Selection.Count != 1;

            foreach (ManipulatorEntity manipEnt in this.Entities)
            {
                IEntity entity = manipEnt.Entity;
                if (!entity.CanSize)
                {
                    continue;
                }

                Matrix2D newMat = manipEnt.StartMatrix;
                Vector2D newPos = newMat.Pos;
                Size2D newSize = manipEnt.StartSize;

                this.smartManipulator.Size_GetAmount(multiSelect ? entity : null, out translateAmount, out sizeAmount);

                float widthUnit = this.CalculateUnitClamp(newSize.Width, sizeAmount.x, EntityUtility.MinSize, EntityUtility.MaxSize);
                float heightUnit = this.CalculateUnitClamp(newSize.Height, sizeAmount.y, EntityUtility.MinSize, EntityUtility.MaxSize);

                newSize.Width = newSize.Width + (sizeAmount.x * widthUnit);
                newSize.Height = newSize.Height + (sizeAmount.y * heightUnit);

                newPos.x = newPos.x + (translateAmount.x * widthUnit);
                newPos.y = newPos.y + (translateAmount.y * heightUnit);

                newMat.Pos = newPos;

                entity.Matrix = newMat;
                entity.Size = newSize;
            }

            this.smartManipulator.Size_GetAmount(null, out translateAmount, out sizeAmount);

            Size2D manipSize = this.Manipulator.DragStartSize;
            Matrix2D manipMatrix = this.Manipulator.DragStartMatrix;

            manipMatrix.Pos = manipMatrix.Pos + translateAmount;

            manipSize.Width = MathUtility.ClampMin(manipSize.Width + sizeAmount.x, 0.0f);
            manipSize.Height = MathUtility.ClampMin(manipSize.Height + sizeAmount.y, 0.0f);

            this.Manipulator.Size = manipSize;
            this.Manipulator.Matrix = manipMatrix;
        }

        /// <summary>
        /// Handle rotate manipulation.
        /// </summary>
        private void HandleRotateUpdate()
        {
            Matrix2D transform = this.smartManipulator.Rotate_GetAmount();

            foreach (ManipulatorEntity manipEnt in this.Entities)
            {
                IEntity entity = manipEnt.Entity;

                entity.Matrix = transform * manipEnt.StartMatrix;
            }

            this.smartManipulator.Matrix = transform * this.Manipulator.DragStartMatrix;
        }

        /// <summary>
        /// Handle translate manipulation.
        /// </summary>
        private void HandleTranslateUpdate()
        {
            Vector2D translate = this.smartManipulator.Translate_GetAmount();
            
            foreach (ManipulatorEntity manipEnt in this.Entities)
            {
                IEntity entity = manipEnt.Entity;
                Matrix2D entityTemp = manipEnt.StartMatrix;
                
                entityTemp.Pos = entityTemp.Pos + translate;

                entity.Matrix = entityTemp;
            }

            Matrix2D temp = this.smartManipulator.DragStartMatrix;

            temp.Pos = temp.Pos + translate;

            this.smartManipulator.Matrix = temp;
        }
        
        /// <summary>
        /// Given a start value and a difference to add to it, calculate a unit value which tells
        /// the called how much of the difference can be applied to the start value given the
        /// passed min and max bounds.
        /// </summary>
        /// <param name="start">Start value.</param>
        /// <param name="diff">Difference to apply to the value.</param>
        /// <param name="min">Clamping min value.</param>
        /// <param name="max">Clamping max value.</param>
        /// <returns>Returns a unit value whith tells the called how much of the passed diff value can be applied
        /// to the start value.</returns>
        private float CalculateUnitClamp(float start, float diff, float min, float max)
        {
            float end = start + diff;
            float change = end - start;
            float absChange = System.Math.Abs(change);

            if (absChange <= MathUtility.Epsilon)
            {
                return 1.0f;
            }

            float unit = 1.0f;
            if (end < min)
            {
                float over = end - min;

                unit = 1.0f - (over / change);
            }
            else if (end > max)
            {
                float over = end - max;

                unit = 1.0f - (over / change);
            }

            return MathUtility.ClampUnit(unit);
        }

        #endregion
    }
}
